<?php
interface iCache {
    public function get($key);
    public function set($key, $value);
}

class SakeStorage {
    protected $url = 'http://sake.gamespy.com/SakeStorageServer/StorageServer.asmx';
    protected $gameid;
    protected $tableid;

    protected $ticket;

    protected $cacheFlag = 0; // seems to be optional

    protected $cacheHandler;


    /**
     *
     * @param string $ticket
     * @param mixed $cache Should implement iCache interface, could be null
     */
    public function  __construct($ticket, $cache=null) {
        $this->ticket = $ticket;
        $this->cacheHandler = $cache;
    }


    /**
     *
     * @param string $action
     * @param array $arr
     * @return string
     */
    protected function CreateSOAPRequest($action, $arr) {
        $arr += array(
            'gameid' => $this->gameid,
            'tableid' => $this->tableid,
            'cacheFlag' => $this->cacheFlag,
            'loginTicket' => $this->ticket,
        );
        $doc = new DOMDocument();
        $doc->loadXML('<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns="http://gamespy.net/sake"><SOAP-ENV:Body/></SOAP-ENV:Envelope>');
        $body = $doc->createElement($action);
        $doc->documentElement->firstChild->appendChild($body);
        foreach($arr as $option => $value) {
            if ($value == null) continue;
            $optionElement = $doc->createElement($option);
            if (is_array($value)) {
                foreach ($value as $z) {
                    if (is_int($z)) {
                        $tp = 'int';
                    } elseif (is_string($z)) {
                        $tp = 'string';
                    }
                    $x = $doc->createElement($tp);
                    $x->appendChild($doc->createTextNode((string) $z));
                    $optionElement->appendChild($x);
                }
            } else {
                $optionElement->appendChild($doc->createTextNode((string) $value));
            }
            $body->appendChild($optionElement);
        }
        return $doc->saveXML();
    }

    protected function checkOptions() {
        /*if (!isset($this->url, $this->tableid, $this->ticket)) {
            throw new SakeStorageAccessError("Not configured", "Either ticket, URL or tableid isn't set");
        }*/
    }


    /**
     *
     * @link http://sake.gamespy.com/SakeStorageServer/StorageServer.asmx?op=SearchForRecords
     *
     * @param array $fields An array with strings, like SELECT [...]
     * @param string $filter Similiar to SQL WHERE [...] statement
     * @param string $sort Similiar to SQL ORDER BY [%field% (DESC|ASC)] statement
     * @param int $offset Similiar to SQL OFFSET [...]
     * @param array $ownerids An array containing integers
     * @param int $max Similiar to SQL LIMIT [...]
     * @param string $targetfilter Like filter, but used with surrounding
     * @param int $surrounding Usable with targetFilter, how many results
     *  around target it should return
     *
     * @return Result
     */
    public function SearchForRecords($fields, $filter=null, $sort=null,
                                     $offset=null, $ownerids=null, $max=50,
                                     $targetfilter=null, $surrounding=null) {
        $this->checkOptions();
        $action = 'SearchForRecords';
        if(isset($this->cacheHandler)) {
            $key = "";
            foreach (array($fields, $filter, $sort, $offset, $ownerids, $max, $targetfilter, $surrounding,
                    $this->gameid, $this->tableid, $this->cacheFlag, $action, ) as $i) {
                $key .= var_export($i, true);
            }
            $key = sha1($key);
            $res = $this->cacheHandler->get($key);
            if ($res !== null) {
                return $res;
            }
        }

        $payload = $this->CreateSOAPRequest($action, array(
            'fields' => $fields,
            'filter' => $filter,
            'sort' => $sort,
            'offset' => $offset,
            'ownerids' => $ownerids,
            'max' => $max,
            'targetfilter' => $targetfilter,
            'surrounding' => $surrounding
        ));

        $res = $this->fetchURL($payload, $action);
        $result = new SFRResult($res);
        unset($now, $res);

        if(isset($this->cacheHandler)) {
            $this->cacheHandler->set($key, $result);
        }

        return $result;

    }

    public function MultiGetRecordCount($filters) {
        $this->checkOptions();
        $action = 'GetRecordCount';
        $cache = isset($this->cacheHandler);

        $results = array();
        $bodies = array();
        $hashes = array();

        foreach ($filters as $k => $filter) {
            if($cache) {
                $key = "";
                foreach (array($filter,
                        $this->gameid, $this->tableid, $this->cacheFlag, $action) as $i) {
                    $key .= var_export($i, true);
                }
                $key = sha1($key);
                $res = $this->cacheHandler->get($key);
                if ($res !== null) {
                    $results[$k] = $res;
                    continue;
                }

                $hashes[$k] = $key;

                $bodies[$k] = $this->CreateSOAPRequest($action,
                    array('filter' => $filter));
            }
        }

        $now = microtime(true);
        $results += $this->multiFetchURL($bodies);

        foreach ($hashes as $k =>$hash) {
            if(isset($cache)) {
                $this->cacheHandler->set($hash, $results[$k]);
            }
        }

        $now = microtime(true) - $now;

        foreach ($results as &$res) {
            $res = new GRCResult($res, microtime(true) - $now);
        }

        return $results;

    }

    public function GetRecordCount($filter) {
        $this->checkOptions();

        $action = 'GetRecordCount';
        if(isset($this->cacheHandler)) {
            $key = "";
            foreach (array($filter,
                    $this->gameid, $this->tableid, $this->cacheFlag, $action) as $i) {
                $key .= var_export($i, true);
            }
            $key = sha1($key);
            $res = $this->cacheHandler->get($key);
            if ($res !== null) {
                return $res;
            }
        }

        $payload = $this->CreateSOAPRequest($action,
            array('filter' => $filter));

        $now = microtime(true);
        $res = $this->fetchURL($payload, $action);
        $result = new GRCResult($res, $now);

        if(isset($this->cacheHandler)) {
            $this->cacheHandler->set($key, $result);
        }

        return $result;
    }

    protected function multiFetchURL($bodies) {
        $curls = array();
        $result = array();

        $mh = curl_multi_init();

        foreach ($bodies as $k => $body) {
            $ch = curl_init($this->url);

            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_HTTPHEADER,
                array('Content-Type: application/soap+xml'));

            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);

            $curls[$k] = $ch;
            curl_multi_add_handle($mh, $ch);
        }

        $active = null;
        do {
            $mrc = curl_multi_exec($mh, $active);
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        while ($active && $mrc == CURLM_OK) {
            if (curl_multi_select($mh) != -1) {
                do {
                    $mrc = curl_multi_exec($mh, $active);
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
            }
        }

        foreach ($curls as $k => $ch) {
            curl_multi_remove_handle($mh, $ch);
            $result[$k] = curl_multi_getcontent($ch);
            //curl_close($ch);
        }

        curl_multi_close($mh);

        return $result;


    }

    protected function fetchURL($body, $action) {

        $ch = curl_init($this->url);

        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array(
            'Content-Type: text/xml',
            "SOAPAction: \"http://gamespy.net/sake/$action\"",
        ));
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        
        $res = curl_exec($ch);

        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
        if ($code !== 200 and $code !== 0 and $code !== 500) {
            curl_close($ch);
            throw new SakeStorageNetworkError("HTTP code {$code}");
        }
        if ($res === false) {
            $error = curl_error($ch);
            curl_close($ch);
            throw new SakeStorageNetworkError($error);
        }
        if (strpos($contentType, 'application/soap+xml') === false &&
            strpos($contentType, 'text/xml') === false) {
            curl_close($ch);
            throw new SakeStorageNetworkError("Content-type is {$contentType}");
        }


        return $res;
    }

    public static function dealWithError($result) {
        switch($result) {
            case 'Success':
                break;
            case 'LoginTicketInvalid':
            case 'LoginTicketExpired':
            case 'DatabaseUnavailable':
                throw new SakeStorageAccessError($result);
            default:
                throw new SakeStorageQueryError($result);
        }
    }

}

class GRCResult {
    protected $value;
    protected $error;

    public function __construct($res) {
        $matches = array();
        if (preg_match("|<soap:Fault>|i", $res)) {
            preg_match("|<soap:Text.*?>(.*?)</soap:Text>|is",
                $res, $matches);

            $this->error = new SakeStorageQueryError($matches[1]);
            return;
        }
        if (!preg_match("|<GetRecordCountResult>(.*?)</GetRecordCountResult>|i",
                $res, $matches)) {
            throw new SakeStorageAccessError('Cannot parse response.');
        }
        try {
            SakeStorage::dealWithError($matches[1]);
        } catch (SakeStorageQueryError $e) {
            $this->error = $e;
            return;
        }

        if (!preg_match("|<count>(.*?)</count>|i",
                $res, $matches)) {
                throw new SakeStorageAccessError('Cannot parse response.');
            }

        $this->value = (int) $matches[1];

    }


    protected function checkForErrors() {
        if (!is_null($this->error)) {
           throw $this->error;
        }
    }

    public function getData() {
        $this->checkForErrors();
        return $this->value;
    }
}
/**
 * This class is used internally, you shouldn't instantiate it directly
 * 
 * SAX-like parser for SearchForRecords SakeStorage method
 */
class SFRResultParser {

    protected $SFRResult;
    protected $faultstring;
    protected $AORV;
    protected $RV;
    protected $val;
    protected $arrayCount;
    protected $fieldCount;
    protected $valText;
    protected $arr;
    protected $fsText;
    protected $SFRRtext;

    protected $parser;

    protected $error;

    protected $data = array();

    /**
     * @param string $xml
     * @param SFRResult $result
     */

    public function  __construct($res) {
        $this->parser = xml_parser_create();
        xml_set_element_handler($this->parser, array($this, 'startElement'), array($this, 'endElement'));
        xml_set_character_data_handler($this->parser, array($this, 'characters'));
        try {
            if (xml_parse($this->parser, $res) !== 1) {
                throw new SakeStorageAccessError(xml_error_string(xml_get_error_code($this->parser)));
            }
        } catch (SakeStorageQueryError $e) {
            $this->error = $e;
        }
    }

    public function  __destruct() {
        xml_parser_free($this->parser);
    }

    protected function startElement($parser, $name, $attrs) {
        switch($name) {
            case 'VALUE': #field value
                $this->val = true;
                $this->valText = '';
                break;
            case 'RECORDVALUE': #field
                $this->RV = true;
                break;
            case 'ARRAYOFRECORDVALUE': #row
                $this->arr = array();
                $this->AORV = true;
                break;
            case 'SEARCHFORRECORDSRESULT':
                $this->SFRResult = true;
                $this->SFRRtext = '';
                break;
            case 'SOAP:TEXT':
                $this->faultstring = true;
                $this->fsText = '';
                break;
        }
    }
    protected function endElement($parser, $name) {
        switch($name) {
            case 'VALUE': #field value
                $this->val = false;
                $this->arr[] = $this->valText;
                break;
            case 'RECORDVALUE': #field
                $this->RV = false;
                $this->fieldCount += 1;
                break;
            case 'ARRAYOFRECORDVALUE': #row
                $this->arrayCount += 1;
                $this->fieldCount = 0;
                $this->AORV = false;
                $this->data[] = $this->arr;
                break;
            case 'SEARCHFORRECORDSRESULT':
                $this->SFRResult = false;
                SakeStorage::dealWithError($this->SFRRtext);
                break;
            case 'SOAP:TEXT':
                $this->faultstring = false;
                if (strpos($this->fsText, 'Timeout expired.') !== false) {
                    throw new SakeStorageAccessError($this->fsText);
                } else {
                    throw new SakeStorageQueryError($this->fsText);
                }
                break;
            }
        }
    protected function characters($parser, $content) {
        if ($this->val) {
            $this->valText .= $content;
        } elseif ($this->SFRResult) {
            $this->SFRRtext .= $content;
        } elseif ($this->faultstring) {
            $this->fsText .= $content;
        }
    }

    public function getData() {
        if (isset($this->error)) {
            throw $this->error;
        }
        return $this->data;
    }


}

class SFRResult {
    protected $data;
    protected $error;


    function __construct($xml) {

        $parser = new SFRResultParser($xml, $this);
        $this->data = $parser->getData();
    }

    public function getData() {
        $this->checkForErrors();
        return $this->data;
    }

    protected function checkForErrors() {
        if (!is_null($this->error)) {
           throw $this->error;
        }
    }
}

/**
 * Base class for SakeStorage errors.
 */
class SakeStorageError extends Exception {

    /**
     *
     * @param string $msg Short description
     */
    public function __construct($msg) {
        parent::__construct($msg);
    }
}

/**
 * Raised if there is an error in query itself.
 *
 */
class SakeStorageQueryError extends SakeStorageError {

    public function  __construct($msg) {
        parent::__construct($msg);
    }
}

/**
 * Raised in case if SakeStorage is inaccessible for some reason. Timeouts,
 * wrong tickets, DNS issues and so on. Be aware that very heavy request could
 * always trigger timeouts, in that case split them up.
 */
class SakeStorageAccessError extends SakeStorageError {
    public function  __construct($msg) {
        parent::__construct($msg);
    }
}


class SakeStorageTimeout extends SakeStorageAccessError {
    public function  __construct() {
        parent::__construct("SakeStorage Internal Timeout");
    }
}
class SakeStorageNetworkError extends SakeStorageAccessError {}
